คู่มือฉบับสมบูรณ์เกี่ยวกับ useContext hook ของ React ครอบคลุมรูปแบบการใช้งาน context และเทคนิคการเพิ่มประสิทธิภาพขั้นสูงสำหรับการสร้างแอปพลิเคชันที่มีประสิทธิภาพและขยายขนาดได้
React useContext: เชี่ยวชาญการใช้งาน Context และการเพิ่มประสิทธิภาพ
Context API ของ React เป็นเครื่องมือที่มีประสิทธิภาพในการแชร์ข้อมูลระหว่างคอมโพเนนต์โดยไม่ต้องส่ง props ผ่านทุกระดับของโครงสร้างคอมโพเนนต์ (component tree) อย่างชัดเจน Hook useContext ช่วยให้การเข้าถึงค่าใน context ง่ายขึ้น ทำให้การเข้าถึงและใช้ข้อมูลที่แชร์กันภายใน functional components เป็นเรื่องสะดวก แต่การใช้งาน useContext ที่ไม่เหมาะสมอาจนำไปสู่ปัญหาคอขวดด้านประสิทธิภาพ โดยเฉพาะในแอปพลิเคชันขนาดใหญ่และซับซ้อน คู่มือนี้จะสำรวจแนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้งาน context และนำเสนอเทคนิคการเพิ่มประสิทธิภาพขั้นสูงเพื่อให้แน่ใจว่าแอปพลิเคชัน React ของคุณมีประสิทธิภาพและสามารถขยายขนาดได้
ทำความเข้าใจ Context API ของ React
ก่อนที่จะเจาะลึกเรื่อง useContext เรามาทบทวนแนวคิดหลักของ Context API กันก่อน Context API ประกอบด้วย 3 ส่วนหลักๆ คือ:
- Context: ตัวเก็บข้อมูลที่ใช้ร่วมกัน คุณสามารถสร้าง context ได้โดยใช้
React.createContext() - Provider: คอมโพเนนต์ที่ส่งค่า context ให้กับคอมโพเนนต์ลูก (descendants) ของมัน คอมโพเนนต์ทั้งหมดที่อยู่ภายใต้ provider จะสามารถเข้าถึงค่า context ได้
- Consumer: คอมโพเนนต์ที่ติดตาม (subscribe) ค่า context และจะทำการ re-render ใหม่ทุกครั้งที่ค่า context เปลี่ยนแปลง ซึ่ง
useContexthook เป็นวิธีที่ทันสมัยในการใช้งาน context ใน functional components
แนะนำ useContext Hook
useContext hook เป็น React hook ที่ช่วยให้ functional components สามารถติดตาม (subscribe) context ได้ มันรับอ็อบเจกต์ context (ค่าที่ได้จากการเรียก React.createContext()) เป็นอาร์กิวเมนต์ และจะคืนค่า context ปัจจุบันสำหรับ context นั้นๆ เมื่อค่า context เปลี่ยนแปลง คอมโพเนนต์ที่ใช้ hook นี้จะทำการ re-render ใหม่
นี่คือตัวอย่างพื้นฐาน:
ตัวอย่างพื้นฐาน
สมมติว่าเรามี theme context:
import React, { createContext, useContext, useState } from 'react';
const ThemeContext = createContext('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme,
};
return (
{children}
);
}
function ThemedComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
Current Theme: {theme}
);
}
function App() {
return (
);
}
export default App;
ในตัวอย่างนี้:
ThemeContextถูกสร้างขึ้นโดยใช้React.createContext('light')โดยมีค่าเริ่มต้นคือ 'light'ThemeProviderทำหน้าที่ส่งค่า theme และฟังก์ชันtoggleThemeให้กับคอมโพเนนต์ลูกThemedComponentใช้useContext(ThemeContext)เพื่อเข้าถึง theme ปัจจุบันและฟังก์ชันtoggleTheme
ข้อผิดพลาดที่พบบ่อยและปัญหาด้านประสิทธิภาพ
แม้ว่า useContext จะช่วยให้การใช้งาน context ง่ายขึ้น แต่ก็อาจก่อให้เกิดปัญหาด้านประสิทธิภาพได้หากไม่ใช้อย่างระมัดระวัง นี่คือข้อผิดพลาดที่พบบ่อยบางประการ:
- การ Re-render ที่ไม่จำเป็น: คอมโพเนนต์ใดๆ ที่ใช้
useContextจะทำการ re-render ใหม่ทุกครั้งที่ค่า context เปลี่ยนแปลง ถึงแม้ว่าคอมโพเนนต์นั้นจะไม่ได้ใช้ส่วนของค่า context ที่เปลี่ยนไปก็ตาม สิ่งนี้อาจนำไปสู่การ re-render ที่ไม่จำเป็นและเกิดปัญหาคอขวดด้านประสิทธิภาพ โดยเฉพาะในแอปพลิเคชันขนาดใหญ่ที่มีการอัปเดตค่า context บ่อยครั้ง - ค่า Context ขนาดใหญ่: หากค่า context เป็นอ็อบเจกต์ขนาดใหญ่ การเปลี่ยนแปลงใดๆ ใน property ของอ็อบเจกต์นั้นจะทำให้คอมโพเนนต์ที่ใช้งาน context ทั้งหมด re-render ใหม่
- การอัปเดตที่บ่อยครั้ง: หากค่า context ถูกอัปเดตบ่อยครั้ง อาจทำให้เกิดการ re-render ต่อเนื่องกันเป็นทอดๆ ทั่วทั้ง component tree ซึ่งส่งผลกระทบต่อประสิทธิภาพ
เทคนิคการเพิ่มประสิทธิภาพ
เพื่อลดปัญหาด้านประสิทธิภาพเหล่านี้ ลองพิจารณาเทคนิคการเพิ่มประสิทธิภาพต่อไปนี้:
1. การแบ่ง Context (Context Splitting)
แทนที่จะรวมข้อมูลที่เกี่ยวข้องทั้งหมดไว้ใน context เดียว ให้แบ่ง context ออกเป็นส่วนเล็กๆ และเฉพาะเจาะจงมากขึ้น วิธีนี้จะช่วยลดจำนวนคอมโพเนนต์ที่จะต้อง re-render เมื่อข้อมูลส่วนใดส่วนหนึ่งเปลี่ยนแปลง
ตัวอย่าง:
แทนที่จะใช้ UserContext เดียวที่เก็บทั้งข้อมูลโปรไฟล์ผู้ใช้และการตั้งค่าผู้ใช้ ให้สร้าง context แยกกันสำหรับแต่ละส่วน:
import React, { createContext, useContext, useState } from 'react';
const UserProfileContext = createContext(null);
const UserSettingsContext = createContext(null);
function UserProfileProvider({ children }) {
const [profile, setProfile] = useState({
name: 'John Doe',
email: 'john.doe@example.com',
});
const updateProfile = (newProfile) => {
setProfile(newProfile);
};
const value = {
profile,
updateProfile,
};
return (
{children}
);
}
function UserSettingsProvider({ children }) {
const [settings, setSettings] = useState({
notificationsEnabled: true,
theme: 'light',
});
const updateSettings = (newSettings) => {
setSettings(newSettings);
};
const value = {
settings,
updateSettings,
};
return (
{children}
);
}
function ProfileComponent() {
const { profile } = useContext(UserProfileContext);
return (
Name: {profile?.name}
Email: {profile?.email}
);
}
function SettingsComponent() {
const { settings } = useContext(UserSettingsContext);
return (
Notifications: {settings?.notificationsEnabled ? 'Enabled' : 'Disabled'}
Theme: {settings?.theme}
);
}
function App() {
return (
);
}
export default App;
ตอนนี้ การเปลี่ยนแปลงโปรไฟล์ผู้ใช้จะทำให้เฉพาะคอมโพเนนต์ที่ใช้งาน UserProfileContext re-render เท่านั้น และการเปลี่ยนแปลงการตั้งค่าผู้ใช้ก็จะทำให้เฉพาะคอมโพเนนต์ที่ใช้งาน UserSettingsContext re-render
2. การทำ Memoization ด้วย React.memo
ห่อหุ้มคอมโพเนนต์ที่ใช้งาน context ด้วย React.memo ซึ่งเป็น higher-order component ที่จะทำการ memoize functional component มันจะป้องกันการ re-render หาก props ของคอมโพเนนต์นั้นไม่เปลี่ยนแปลง เมื่อใช้ร่วมกับการแบ่ง context จะสามารถลดการ re-render ที่ไม่จำเป็นได้อย่างมาก
ตัวอย่าง:
import React, { useContext } from 'react';
const MyContext = React.createContext(null);
const MyComponent = React.memo(function MyComponent() {
const { value } = useContext(MyContext);
console.log('MyComponent rendered');
return (
Value: {value}
);
});
export default MyComponent;
ในตัวอย่างนี้ MyComponent จะ re-render ก็ต่อเมื่อ value ใน MyContext เปลี่ยนแปลงเท่านั้น
3. การใช้ useMemo และ useCallback
ใช้ useMemo และ useCallback เพื่อ memoize ค่าและฟังก์ชันที่จะส่งเป็นค่าของ context วิธีนี้จะช่วยให้แน่ใจว่าค่า context จะเปลี่ยนแปลงก็ต่อเมื่อ dependencies ที่เกี่ยวข้องเปลี่ยนแปลงเท่านั้น ซึ่งจะป้องกันการ re-render ที่ไม่จำเป็นของคอมโพเนนต์ที่ใช้งาน context
ตัวอย่าง:
import React, { createContext, useState, useMemo, useCallback, useContext } from 'react';
const MyContext = createContext(null);
function MyProvider({ children }) {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
const contextValue = useMemo(() => ({
count,
increment,
}), [count, increment]);
return (
{children}
);
}
function MyComponent() {
const { count, increment } = useContext(MyContext);
console.log('MyComponent rendered');
return (
Count: {count}
);
}
function App() {
return (
);
}
export default App;
ในตัวอย่างนี้:
useCallbackทำการ memoize ฟังก์ชันincrementเพื่อให้แน่ใจว่าฟังก์ชันจะถูกสร้างขึ้นใหม่ก็ต่อเมื่อ dependencies เปลี่ยนแปลง (ในกรณีนี้ ไม่มี dependencies ดังนั้นมันจึงถูก memoize ตลอดไป)useMemoทำการ memoize ค่าของ context เพื่อให้แน่ใจว่าค่าจะเปลี่ยนแปลงก็ต่อเมื่อcountหรือฟังก์ชันincrementเปลี่ยนแปลงเท่านั้น
4. การใช้ Selectors
สร้าง selectors เพื่อดึงข้อมูลเฉพาะส่วนที่จำเป็นจากค่า context ภายในคอมโพเนนต์ที่ใช้งาน ซึ่งจะช่วยลดโอกาสในการ re-render ที่ไม่จำเป็น โดยทำให้แน่ใจว่าคอมโพเนนต์จะ re-render ก็ต่อเมื่อข้อมูลที่มันต้องใช้เปลี่ยนแปลงจริงๆ เท่านั้น
ตัวอย่าง:
import React, { createContext, useContext } from 'react';
const MyContext = createContext(null);
const selectCount = (contextValue) => contextValue.count;
function MyComponent() {
const contextValue = useContext(MyContext);
const count = selectCount(contextValue);
console.log('MyComponent rendered');
return (
Count: {count}
);
}
export default MyComponent;
แม้ว่าตัวอย่างนี้จะดูเรียบง่าย แต่ในสถานการณ์จริง selectors อาจมีความซับซ้อนและมีประสิทธิภาพมากกว่า โดยเฉพาะเมื่อต้องจัดการกับค่า context ขนาดใหญ่
5. โครงสร้างข้อมูลแบบ Immutable
การใช้โครงสร้างข้อมูลแบบ immutable จะช่วยให้แน่ใจว่าการเปลี่ยนแปลงค่า context จะสร้างอ็อบเจกต์ใหม่ขึ้นมาแทนที่การแก้ไขอ็อบเจกต์เดิม ซึ่งทำให้ React ตรวจจับการเปลี่ยนแปลงและเพิ่มประสิทธิภาพการ re-render ได้ง่ายขึ้น ไลบรารีอย่าง Immutable.js สามารถช่วยในการจัดการโครงสร้างข้อมูลแบบ immutable ได้
ตัวอย่าง:
import React, { createContext, useState, useMemo, useContext } from 'react';
import { Map } from 'immutable';
const MyContext = createContext(Map());
function MyProvider({ children }) {
const [data, setData] = useState(Map({
count: 0,
name: 'Initial Name',
}));
const increment = () => {
setData(prevData => prevData.set('count', prevData.get('count') + 1));
};
const updateName = (newName) => {
setData(prevData => prevData.set('name', newName));
};
const contextValue = useMemo(() => ({
data,
increment,
updateName,
}), [data]);
return (
{children}
);
}
function MyComponent() {
const contextValue = useContext(MyContext);
const count = contextValue.get('count');
console.log('MyComponent rendered');
return (
Count: {count}
);
}
function App() {
return (
);
}
export default App;
ตัวอย่างนี้ใช้ Immutable.js ในการจัดการข้อมูล context เพื่อให้แน่ใจว่าการอัปเดตแต่ละครั้งจะสร้าง Map ใหม่ที่ไม่สามารถเปลี่ยนแปลงได้ ซึ่งช่วยให้ React เพิ่มประสิทธิภาพการ re-render ได้ดียิ่งขึ้น
ตัวอย่างการใช้งานจริง
Context API และ useContext ถูกนำไปใช้อย่างแพร่หลายในสถานการณ์จริงต่างๆ:
- การจัดการ Theme: ดังที่แสดงในตัวอย่างก่อนหน้า คือการจัดการ theme (โหมดสว่าง/มืด) ทั่วทั้งแอปพลิเคชัน
- การยืนยันตัวตน (Authentication): การส่งสถานะการยืนยันตัวตนและข้อมูลผู้ใช้ให้กับคอมโพเนนต์ที่ต้องการ ตัวอย่างเช่น global authentication context สามารถจัดการการล็อกอิน, ล็อกเอาต์, และข้อมูลโปรไฟล์ผู้ใช้ ทำให้สามารถเข้าถึงได้ทั่วทั้งแอปพลิเคชันโดยไม่ต้องทำ prop drilling
- การตั้งค่าภาษา/ภูมิภาค (Language/Locale): การแชร์การตั้งค่าภาษาหรือภูมิภาคปัจจุบันทั่วทั้งแอปพลิเคชันสำหรับการทำ internationalization (i18n) และ localization (l10n) ซึ่งช่วยให้คอมโพเนนต์สามารถแสดงเนื้อหาในภาษาที่ผู้ใช้ต้องการได้
- การกำหนดค่าส่วนกลาง (Global Configuration): การแชร์การตั้งค่าส่วนกลาง เช่น API endpoints หรือ feature flags ซึ่งสามารถใช้เพื่อปรับเปลี่ยนพฤติกรรมของแอปพลิเคชันแบบไดนามิกตามการตั้งค่า
- ตะกร้าสินค้า (Shopping Cart): การจัดการ state ของตะกร้าสินค้าและให้สิทธิ์การเข้าถึงรายการสินค้าและการดำเนินการต่างๆ ให้กับคอมโพเนนต์ทั่วทั้งแอปพลิเคชันอีคอมเมิร์ซ
ตัวอย่าง: การทำ Internationalization (i18n)
ลองดูตัวอย่างง่ายๆ ของการใช้ Context API สำหรับการทำ internationalization:
import React, { createContext, useState, useContext, useMemo } from 'react';
const LanguageContext = createContext({
locale: 'en',
messages: {},
});
const translations = {
en: {
greeting: 'Hello',
description: 'Welcome to our website!',
},
fr: {
greeting: 'Bonjour',
description: 'Bienvenue sur notre site web !',
},
es: {
greeting: 'Hola',
description: '¡Bienvenido a nuestro sitio web!',
},
};
function LanguageProvider({ children }) {
const [locale, setLocale] = useState('en');
const setLanguage = (newLocale) => {
setLocale(newLocale);
};
const messages = useMemo(() => translations[locale] || translations['en'], [locale]);
const contextValue = useMemo(() => ({
locale,
messages,
setLanguage,
}), [locale, messages]);
return (
{children}
);
}
function Greeting() {
const { messages } = useContext(LanguageContext);
return (
{messages.greeting}
);
}
function Description() {
const { messages } = useContext(LanguageContext);
return (
{messages.description}
);
}
function LanguageSwitcher() {
const { setLanguage } = useContext(LanguageContext);
return (
);
}
function App() {
return (
);
}
export default App;
ในตัวอย่างนี้:
LanguageContextทำหน้าที่ส่ง locale และข้อความปัจจุบันLanguageProviderจัดการ state ของ locale และส่งค่า context- คอมโพเนนต์
GreetingและDescriptionใช้ context เพื่อแสดงข้อความที่แปลแล้ว - คอมโพเนนต์
LanguageSwitcherช่วยให้ผู้ใช้สามารถเปลี่ยนภาษาได้
ทางเลือกอื่นนอกเหนือจาก useContext
แม้ว่า useContext จะเป็นเครื่องมือที่มีประสิทธิภาพ แต่มันก็ไม่ใช่ทางออกที่ดีที่สุดสำหรับทุกสถานการณ์ในการจัดการ state นี่คือทางเลือกอื่นๆ ที่น่าพิจารณา:
- Redux: state container ที่คาดเดาได้สำหรับแอป JavaScript Redux เป็นตัวเลือกที่นิยมสำหรับการจัดการ state ของแอปพลิเคชันที่ซับซ้อน โดยเฉพาะในแอปพลิเคชันขนาดใหญ่
- MobX: โซลูชันการจัดการ state ที่เรียบง่ายและขยายขนาดได้ MobX ใช้ข้อมูลที่สามารถสังเกตการณ์ได้ (observable data) และการตอบสนองอัตโนมัติ (automatic reactivity) ในการจัดการ state
- Recoil: ไลบรารีการจัดการ state สำหรับ React ที่ใช้ atoms และ selectors ในการจัดการ state Recoil ถูกออกแบบมาให้มีความละเอียดและมีประสิทธิภาพมากกว่า Redux หรือ MobX
- Zustand: โซลูชันการจัดการ state ที่เล็ก, รวดเร็ว และขยายขนาดได้ โดยใช้หลักการ flux ที่เรียบง่าย
- Jotai: การจัดการ state ที่พื้นฐานและยืดหยุ่นสำหรับ React ด้วยโมเดลแบบ atomic
- Prop Drilling: ในกรณีที่ไม่ซับซ้อนซึ่ง component tree ไม่ลึกมาก การทำ prop drilling อาจเป็นทางเลือกที่เหมาะสม ซึ่งคือการส่ง props ผ่านคอมโพเนนต์หลายระดับลงไป
การเลือกโซลูชันการจัดการ state ขึ้นอยู่กับความต้องการเฉพาะของแอปพลิเคชันของคุณ ควรพิจารณาความซับซ้อนของแอปพลิเคชัน, ขนาดของทีม, และข้อกำหนดด้านประสิทธิภาพในการตัดสินใจ
สรุป
useContext hook ของ React เป็นวิธีที่สะดวกและมีประสิทธิภาพในการแชร์ข้อมูลระหว่างคอมโพเนนต์ การทำความเข้าใจข้อผิดพลาดด้านประสิทธิภาพที่อาจเกิดขึ้นและการใช้เทคนิคการเพิ่มประสิทธิภาพที่กล่าวถึงในคู่มือนี้ จะช่วยให้คุณสามารถใช้ประโยชน์จาก useContext เพื่อสร้างแอปพลิเคชัน React ที่ขยายขนาดได้และมีประสิทธิภาพสูง อย่าลืมแบ่ง context เมื่อเหมาะสม, ทำ memoize คอมโพเนนต์ด้วย React.memo, ใช้ useMemo และ useCallback สำหรับค่า context, สร้าง selectors, และพิจารณาใช้โครงสร้างข้อมูลแบบ immutable เพื่อลดการ re-render ที่ไม่จำเป็นและเพิ่มประสิทธิภาพของแอปพลิเคชันของคุณ
ควรทำการวัดประสิทธิภาพ (profile) ของแอปพลิเคชันอยู่เสมอเพื่อระบุและแก้ไขปัญหาคอขวดที่เกี่ยวข้องกับการใช้ context การปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุดเหล่านี้จะช่วยให้แน่ใจว่าการใช้ useContext ของคุณจะส่งผลให้ผู้ใช้ได้รับประสบการณ์ที่ราบรื่นและมีประสิทธิภาพ